<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多人视频会议</title>
    <script src="https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js"></script>
    <script>
      new VConsole()
    </script>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="https://iapp.becdn.net/lib/vue.min.js"></script>
    <style>
        /* Login page styles */
        .login-page {
            font-family: Arial, sans-serif;
            background-color: #f2f2f2;
            margin: 0;
            padding: 0;
        }

        .login-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            background-color: #fff;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            height: 100vh;
        }

        .input-group {
            margin-bottom: 10px;
        }

        input[type="text"] {
            width: 100%;
            padding: 10px;
            border-radius: 4px;
            border: 1px solid #ccc;
            font-size: 16px;
        }

        .join-button {
            background-color: #007BFF;
            color: #fff;
            padding: 10px 20px;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
        }

        .join-button:hover {
            background-color: #0069d9;
        }

        /* Video page styles */
        .video-page {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
        }

        .video-container {
            width: 100%;
            max-width: 960px;
            padding: 20px;
            box-sizing: border-box;
        }

        .video-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(2.jpg, 1fr));
            grid-gap: 10px;
            justify-items: center;
        }

        .video-item {
            object-fit: cover;
            width: 100%;
            height: 100%;
            max-height: 300px;
        }

        .controls {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-between;
            align-items: center;
            margin-top: 20px;
        }

        .control-button {
            background-color: #007aff;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 8px 16px;
            cursor: pointer;
            font-size: 14px;
            outline: none;
            flex-basis: calc(33.33% - .png);
        }

        @media (max-width: 600px) {
            .controls {
                flex-direction: column;
            }

            .control-button {
                flex-basis: 100%;
                margin-bottom: 10px;
            }

            .media-control {
                display: flex;
                flex-direction: column;
                align-items: center;
            }
        }

        .control-button:hover {
            background-color: #0062cc;
        }

        /** 聊天公屏styles **/
        .chat-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 300px;
            max-height: calc(100vh - ½0px);
            background-color: #fff;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            transition: transform 0.3s ease-in-out;
            z-index: 1000;
        }

        .chat-panel.collapsed {
            transform: translateX(100%);
        }

        .chat-header {
            padding: 10px;
            background-color: #f5f5f5;
            font-weight: bold;
            border-bottom: 1px solid #ddd;
        }

        .chat-body {
            max-height: 200px;
            overflow-y: auto;
            padding: 10px;
        }

        .chat-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            border-top: 1px solid #ddd;
        }

        .chat-footer textarea {
            flex-grow: 1;
            padding: ¾px;
            border: none;
            border-radius: 4px;
            font-size: 14px;
            height: 60px;
            resize: none;
        }

        .send-button {
            background-color: #007aff;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 8px 16px;
            cursor: pointer;
            font-size: 14px;
            outline: none;
            flex-basis: calc(33.33% - .png);
            margin-left: 5px;
        }
    </style>
</head>
<body>
<div id="app">
    <!-- Login page -->
    <div class="login-page" v-if="showLoginPage">
        <div class="login-container">
            <h2>Join the Room</h2>
            <div class="input-group">
                <label for="room">房间号:</label>
                <input type="text" id="room" placeholder="请输入房间号" v-model="room"/>
            </div>
            <div class="input-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" placeholder="请输入用户名" v-model="username"/>
            </div>
            <button class="join-button" @click="join">加入</button>
        </div>
    </div>

    <!-- Video page -->
    <div class="video-page" v-if="showVideoPage">
        <div class="video-container">
            <h2>Video Conference</h2>
            <div class="video-grid">
                <video class="video-item" webkit-playsinline playsinline muted ref="localVideo">
                    <source src="" type="video/mp4">
                </video>
                <video class="video-item" webkit-playsinline playsinline
                       v-for="stream in remoteStreams" :key="stream.id" ref="remoteVideo">
                    <source src="" type="video/mp4">
                </video>
            </div>
            <div class="controls">
                <div class="media-control">
                    <button class="control-button" v-bind:style="{'background-color': !audioEnabled ? 'gray' : ''}"
                            @click="changeAudio">
                        {{audioEnabled ? '关闭' : '开启'}}麦克风
                    </button>
                    <button class="control-button" v-bind:style="{'background-color': !videoEnabled ? 'gray' : ''}"
                            @click="changeVideo">
                        {{videoEnabled ? '关闭' : '开启'}}摄像头
                    </button>
                </div>
                <button class="control-button" @click="leave">退出会议</button>
            </div>
        </div>
        <!-- Chat panel -->
        <div class="chat-panel" :class="{ collapsed: chatCollapsed }">
            <div class="chat-header" @click="toggleChatPanel">聊天公屏</div>
            <div class="chat-body" ref="chatBody">
                <div v-for="e in allMessage" v-html="`用户${e.user} - ${e.message}`"></div>
            </div>
            <div class="chat-footer">
                <textarea placeholder="输入消息" v-model="messageInput"></textarea>
                <button class="send-button" @click="sendMessage">发送</button>
            </div>
        </div>
    </div>
</div>
</body>
<script src="simplewebrtc.js"></script>
<script>
  new Vue({
    el: '#app',
    data: {
      showLoginPage: true,
      showVideoPage: false,
      room: null,
      username: null,
      audioEnabled: true,
      videoEnabled: true,
      localStream: null,
      remoteStreams: [],
      rtc: null,
      chatCollapsed: true,
      messageInput: '',
      allMessage: []
    },
    methods: {
      join() {
        if (this.room && this.username) {
          this.initVideo()
        }
      },
      async initVideo() {
        this.localStream = await navigator.mediaDevices.getUserMedia({
          audio: this.audioEnabled ? {
            echoCancellation: {exact: true}, // 强制启用回声消除
            autoGainControl: true,            // 可选：自动增益控制
            noiseSuppression: true           // 可选：噪声抑制
          } : false,
          video: this.videoEnabled
        })

        this.showLoginPage = false
        this.showVideoPage = true
        await this.$nextTick()

        let localVideo = this.$refs.localVideo
        localVideo.srcObject = this.localStream;
        localVideo.play() //先让自己的帅脸出现在屏幕上

        const rtc = new SimpleWebRTC({
          // 这里，实际业务来说应该是room和token两个参数，这里示例就让用户自己输入用户名了
          // room可以是到时具体业务的id（比如咨询预约的id），它是一个逻辑概念
          wsUrl: `wss://api.szcyhj.com/webrtc-demo/o2o?room=${this.room}&username=${this.username}`,
          // wsUrl: `ws://localhost:3333/o2o?room=${this.room}&username=${this.username}`,
          rtcConfig: {
            iceServers: [
              //google的免费打洞服务器，如果大家都在同一个局域网下是不需要打洞的
              // {
              //   urls: ["stun:stun.l.google.com:19302"]
              // },
              //自己搭的打洞服务器
              {
                urls: ["turn:49.234.51.217:3478"],
                username: "admin",
                credential: "admin"
              }
            ]
          },
          localStream: this.localStream,
          remoteStreams: this.remoteStreams
        })

        const self = this
        rtc.onjoined = function (joinedUser) {
          //这里的用户数组都是用户自己输入的用户名，实际业务中大概率会改成用户id，也不用前端输入，而是后端根据token解析之后返回的
          console.log('当前房间用户：%o', this.users)
        }

        rtc.onleaved = function (leavedUser) {
          console.log('当前房间用户：%o', this.users)
        }

        rtc.onclose = function (e) {
          // 这里是后端做的限制，如果房间人数达到上限，则会触发这个错误，前端必须要处理的
          if (e.code === 1008) {
            alert('房间人数已满')
            location.reload()
          }
        }

        rtc.onmessage = function (type, data, sender) {
          if (type === 'chat') {
            self.allMessage.push({
              user: sender,
              message: data.replaceAll('\n', '<br/>')
            })
          }
        }

        this.rtc = rtc
      },
      /**
       * 改变音频轨道。这里有个误区，就是改变本地的音频轨道后，远端其实也会跟着改变的，这是由RTC处理的，并不需要重新推流
       */
      changeAudio() {
        this.audioEnabled = !this.audioEnabled
        let audio = this.localStream.getAudioTracks()[0]
        audio.enabled = this.audioEnabled
      },
      /**
       * 改变视频轨道。同上面一个逻辑
       */
      changeVideo() {
        this.videoEnabled = !this.videoEnabled
        let track = this.localStream.getVideoTracks()[0]
        track.enabled = this.videoEnabled
      },
      /**
       * 退出会议，这里简单示例就直接刷新页面了，实际业务可能要做一些更复杂的处理
       */
      leave() {
        location.reload()
      },
      toggleChatPanel() {
        this.chatCollapsed = !this.chatCollapsed;
      },
      sendMessage() {
        if (!this.messageInput) return
        this.allMessage.push({
          user: this.username,
          message: this.messageInput.replaceAll('\n', '<br/>')
        })
        this.rtc.socket.convertAndSend('chat', this.messageInput)
        this.messageInput = ''
      }
    },
    watch: {
      async remoteStreams(newVal) {
        await this.$nextTick()
        for (let i = 0; i < newVal.length; i++) {
          let stream = newVal[i]
          let remoteVideo = this.$refs.remoteVideo[i]
          if (!remoteVideo.srcObject) {
            remoteVideo.srcObject = stream
            console.log(`播放用户${stream.user}的流`, stream)
            remoteVideo.play()
          }
        }
      },
      async allMessage() {
        await this.$nextTick()
        this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight
      }
    }
  })
</script>
</html>